/*
 * Unidata Platform Community Edition
 * Copyright (c) 2013-2020, UNIDATA LLC, All rights reserved.
 * This file is part of the Unidata Platform Community Edition software.
 *
 * Unidata Platform Community Edition is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Unidata Platform Community Edition is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 */
package org.unidata.mdm.dq.core.service.impl.instance;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;

import org.apache.commons.lang3.StringUtils;
import org.unidata.mdm.core.service.CustomPropertiesSupport;
import org.unidata.mdm.core.service.UPathService;
import org.unidata.mdm.core.type.model.instance.AbstractModelInstanceImpl;
import org.unidata.mdm.dq.core.configuration.DataQualityModelIds;
import org.unidata.mdm.dq.core.service.impl.CleanseFunctionCacheComponent;
import org.unidata.mdm.dq.core.type.model.instance.CleanseFunctionElement;
import org.unidata.mdm.dq.core.type.model.instance.CompositeFunctionElement;
import org.unidata.mdm.dq.core.type.model.instance.DataQualityInstance;
import org.unidata.mdm.dq.core.type.model.instance.FunctionGroupElement;
import org.unidata.mdm.dq.core.type.model.instance.GroovyFunctionElement;
import org.unidata.mdm.dq.core.type.model.instance.JavaFunctionElement;
import org.unidata.mdm.dq.core.type.model.instance.MappingSetElement;
import org.unidata.mdm.dq.core.type.model.instance.NamespaceAssignmentElement;
import org.unidata.mdm.dq.core.type.model.instance.PythonFunctionElement;
import org.unidata.mdm.dq.core.type.model.instance.QualityRuleElement;
import org.unidata.mdm.dq.core.type.model.source.CleanseFunctionGroup;
import org.unidata.mdm.dq.core.type.model.source.CompositeCleanseFunctionSource;
import org.unidata.mdm.dq.core.type.model.source.DataQualityModel;
import org.unidata.mdm.dq.core.type.model.source.GroovyCleanseFunctionSource;
import org.unidata.mdm.dq.core.type.model.source.JavaCleanseFunctionSource;
import org.unidata.mdm.dq.core.type.model.source.PythonCleanseFunctionSource;
import org.unidata.mdm.dq.core.util.DQUtils;
import org.unidata.mdm.system.type.namespace.NameSpace;

/**
 * @author Mikhail Mikhailov on Oct 6, 2020
 * DQ stuff holder.
 */
public class DataQualityInstanceImpl extends AbstractModelInstanceImpl<DataQualityModel>
    implements DataQualityInstance, CustomPropertiesSupport {
    /**
     * Default root group.
     */
    public static final CleanseFunctionGroup DEFAULT_ROOT_GROUP = new CleanseFunctionGroup()
        .withName(DQUtils.ROOT_FUNCTION_GROUP_NAME)
        .withDisplayName("change.default.group.name");
    /**
     * Functions.
     */
    private final Map<String, CleanseFunctionElement> functions = new HashMap<>();
    /**
     * Function groups.
     */
    private final Map<String, FunctionGroupElement> groups = new HashMap<>();
    /**
     * Rules.
     */
    private final Map<String, QualityRuleElement> rules;
    /**
     * Sets.
     */
    private final Map<String, MappingSetElement> sets;
    /**
     * Assignments.
     */
    private final Map<String, NamespaceAssignmentElement> assignments;
    /**
     * The root group.
     */
    private final FunctionGroupElement root;
    /**
     * Constructor.
     */
    public DataQualityInstanceImpl(DataQualityModel dqm, CleanseFunctionCacheComponent cfcc, UPathService ups) {
        super(dqm);

        // Java.
        functions.putAll(
            dqm.getJavaFunctions().stream()
                .map(JavaCleanseFunctionImpl::new)
                .map(jcfi -> { jcfi.implement(getStorageId(), cfcc);  return jcfi; })
                .collect(Collectors.toMap(JavaFunctionElement::getId, Function.identity())));

        // Groovy
        functions.putAll(
            dqm.getGroovyFunctions().stream()
                .map(GroovyCleanseFunctionImpl::new)
                .map(jcfi -> { jcfi.implement(getStorageId(), cfcc);  return jcfi; })
                .collect(Collectors.toMap(GroovyFunctionElement::getId, Function.identity())));

        // Python
        functions.putAll(
            dqm.getPythonFunctions().stream()
                .map(PythonCleanseFunctionImpl::new)
                .map(jcfi -> { jcfi.implement(getStorageId(), cfcc);  return jcfi; })
                .collect(Collectors.toMap(PythonFunctionElement::getId, Function.identity())));

        // Composite
        functions.putAll(
            dqm.getCompositeFunctions().stream()
                .map(s -> new CompositeCleanseFunctionImpl(this, s))
                .map(jcfi -> { jcfi.implement();  return jcfi; })
                .collect(Collectors.toMap(CompositeFunctionElement::getId, Function.identity())));

        // Groups
        CleanseFunctionGroup group = dqm.getFunctionGroup();
        if (group == null) {
            group = DEFAULT_ROOT_GROUP;
        }

        this.root = new FunctionGroupImpl(StringUtils.EMPTY, group, this);
        putGroups(this.root);

        functions.values().forEach(fn -> {

            AbstractCleanseFunctionImpl<?> acfi = (AbstractCleanseFunctionImpl<?>) fn;
            String groupName = Objects.isNull(acfi.getSource().getGroupName())
                    ? DEFAULT_ROOT_GROUP.getName()
                    : acfi.getSource().getGroupName();

            FunctionGroupImpl fgi = (FunctionGroupImpl) groups.get(groupName);
            if (Objects.nonNull(fgi)) {
                fgi.addFunction(fn);
            }
        });

        rules = dqm.getRules().stream()
            .map(r -> new QualityRuleImpl(this, r))
            .collect(Collectors.toMap(QualityRuleElement::getId, Function.identity()));

        sets = dqm.getSets().stream()
            .map(s -> new MappingSetImpl(ups, this, s))
            .collect(Collectors.toMap(MappingSetElement::getId, Function.identity()));

        assignments = dqm.getAssignments().stream()
            .map(nsa -> new NamespaceAssignmentImpl(this, nsa))
            .filter(nsa -> Objects.nonNull(nsa.getNameSpace()))
            .collect(Collectors.toMap(NamespaceAssignmentElement::getId, Function.identity()));
    }

    private void putGroups(FunctionGroupElement root) {
        groups.put(root.getPath(), root);
        for (FunctionGroupElement el : root.getChildren()) {
            putGroups(el);
        }
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public String getInstanceId() {
        return DQUtils.DEFAULT_MODEL_INSTANCE_ID;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public String getTypeId() {
        return DataQualityModelIds.DATA_QUALITY;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isEmpty() {
        return functions.isEmpty();
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public FunctionGroupElement getRootGroup() {
        return root;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public Collection<FunctionGroupElement> getGroups() {
        return groups.values();
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public FunctionGroupElement getGroup(String id) {
        return groups.get(id);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public Collection<CleanseFunctionElement> getFunctions() {
        return functions.values();
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public CleanseFunctionElement getFunction(String id) {
        return functions.get(id);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean functionExists(String id) {
        return functions.containsKey(id);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public Collection<QualityRuleElement> getRules() {
        return rules.values();
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public QualityRuleElement getRule(String id) {
        return rules.get(id);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean ruleExists(String id) {
        return rules.containsKey(id);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public Collection<MappingSetElement> getSets() {
        return sets.values();
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public MappingSetElement getSet(String id) {
        return sets.get(id);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean setExists(String id) {
        return sets.containsKey(id);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public Collection<NamespaceAssignmentElement> getAssignments() {
        return assignments.values();
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public NamespaceAssignmentElement getAssignment(NameSpace ns) {
        return Objects.isNull(ns) ? null : getAssignment(ns.getId());
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public NamespaceAssignmentElement getAssignment(String ns) {
        return assignments.get(ns);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean hasAssignment(NameSpace ns) {
        return Objects.nonNull(ns) && getAssignment(ns.getId()) != null;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public DataQualityModel toSource() {

        List<JavaCleanseFunctionSource> javaFunctions = new ArrayList<>();
        List<GroovyCleanseFunctionSource> groovyFunctions = new ArrayList<>();
        List<PythonCleanseFunctionSource> pythonFunctions = new ArrayList<>();
        List<CompositeCleanseFunctionSource> compositeFunctions = new ArrayList<>();

        functions.values().forEach(fn -> {
            if (fn.isCompositeFunction()) {
                compositeFunctions.add(((CompositeCleanseFunctionImpl) fn).getSource());
            } else if (fn.isGroovyFunction()) {
                groovyFunctions.add(((GroovyCleanseFunctionImpl) fn).getSource());
            } else if (fn.isJavaFunction()) {
                javaFunctions.add(((JavaCleanseFunctionImpl) fn).getSource());
            } else if (fn.isPythonFunction()) {
                pythonFunctions.add(((PythonCleanseFunctionImpl) fn).getSource());
            }
        });

        return new DataQualityModel()
                .withDescription(getDescription())
                .withDisplayName(getDisplayName())
                .withName(getName())
                .withStorageId(getStorageId())
                .withCreateDate(getCreateDate())
                .withCreatedBy(getCreatedBy())
                .withVersion(getVersion())
                .withCustomProperties(getSourceCustomProperties())
                .withJavaFunctions(javaFunctions)
                .withCompositeFunctions(compositeFunctions)
                .withGroovyFunctions(groovyFunctions)
                .withPythonFunctions(pythonFunctions)
                .withFunctionGroup(((FunctionGroupImpl) root).getSource())
                .withRules(rules.values().stream().map(QualityRuleImpl.class::cast).map(QualityRuleImpl::getSource).collect(Collectors.toList()))
                .withSets(sets.values().stream().map(MappingSetImpl.class::cast).map(MappingSetImpl::getSource).collect(Collectors.toList()))
                .withAssignments(assignments.values().stream().map(NamespaceAssignmentImpl.class::cast).map(NamespaceAssignmentImpl::getSource).collect(Collectors.toList()));
    }
}
